// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package buildid

import (
	"bytes"
	"cmd/internal/codesign"
	"crypto/sha256"
	"debug/macho"
	"fmt"
	"io"
)

// FindAndHash读取所有r并返回id出现的偏移量。
// 读取时，FindAndHash还计算并返回r内容的哈希，但id出现的地方被零替换。
// FindAndHash一次从r读取bufSize字节。
// 如果bufSize==0，FindAndHash将使用合理的默认值。
func FindAndHash(r io.Reader, id string, bufSize int) (matches []int64, hash [32]byte, err error) {
	if bufSize == 0 {
		bufSize = 31 * 1024 // bufSize+little可能适合32 kB 
	}
	if len(id) == 0 {
		return nil, [32]byte{}, fmt.Errorf("buildid.FindAndHash: no id specified")
	}
	if len(id) > bufSize {
		return nil, [32]byte{}, fmt.Errorf("buildid.FindAndHash: buffer too small")
	}
	zeros := make([]byte, len(id))
	idBytes := []byte(id)

	// 对于Mach-O文件，我们希望排除代码签名。
	// 代码签名包含整个文件的哈希值（签名
	// 本身除外），包括buildid。因此buildid不能包含签名。
	r = excludeMachoCodeSignature(r)

	// 策略是通过buf读取文件，查找id，
	// 但是我们需要担心如果id被分解
	// 并通过两次不同的读取部分返回，会发生什么。
	// 我们在内存中相邻分配一个小缓冲区（至少len（id））和一个大缓冲区（bufSize字节）
	// 然后将
	// 的尾部复制到小缓冲区，然后再将新数据读入大缓冲区。
	// id搜索覆盖整个小+大缓冲区。
	tiny := (len(id) + 127) &^ 127 // 四舍五入到128对齐
	buf := make([]byte, tiny+bufSize)
	h := sha256.New()
	start := tiny
	for offset := int64(0); ; {
		// 循环维护的文件偏移量对应于&buf[tiny]。
		// buf[start:tiny]是上一次迭代的遗留内容。
		// 将n个字节读入buf[tiny:]后，我们处理buf[start:tiny+n]。
		n, err := io.ReadFull(r, buf[tiny:])
		if err != io.ErrUnexpectedEOF && err != io.EOF && err != nil {
			return nil, [32]byte{}, err
		}

		// 处理所有匹配项。
		for {
			i := bytes.Index(buf[start:tiny+n], idBytes)
			if i < 0 {
				break
			}
			matches = append(matches, offset+int64(start+i-tiny))
			h.Write(buf[start : start+i])
			h.Write(zeros)
			start += i + len(id)
		}
		if n < bufSize {
			// 未填充缓冲区，必须位于文件末尾。
			h.Write(buf[start : tiny+n])
			break
		}

		// 处理除buf的最后一个小字节外的所有字节（bufSize=len（buf）-tiny）。
		// 请注意，如果在
		// 上方搜索发现一个以最后一个微小边缘结尾的id，则start>len（buf）-tiny是可能的。没关系。
		if start < len(buf)-tiny {
			h.Write(buf[start : len(buf)-tiny])
			start = len(buf) - tiny
		}

		// 将微小边缘的末端滑动到缓冲区的开头。
		copy(buf[0:], buf[bufSize:])
		start -= bufSize
		offset += int64(bufSize)
	}
	h.Sum(hash[:0])
	return matches, hash, nil
}

func Rewrite(w io.WriterAt, pos []int64, id string) error {
	b := []byte(id)
	for _, p := range pos {
		if _, err := w.WriteAt(b, p); err != nil {
			return err
		}
	}

	// 更新Mach-O代码签名（如果有）。
	if f, cmd, ok := findMachoCodeSignature(w); ok {
		if codesign.Size(int64(cmd.Dataoff), "a.out") == int64(cmd.Datasize) {
			// 如果大小匹配，则更新签名，因此我们不需要修复头。Go链接器生成的二进制文件应具有
			// 预期大小。否则跳过。
			text := f.Segment("__TEXT")
			cs := make([]byte, cmd.Datasize)
			codesign.Sign(cs, w.(io.Reader), "a.out", int64(cmd.Dataoff), int64(text.Offset), int64(text.Filesz), f.Type == macho.TypeExec)
			if _, err := w.WriteAt(cs, int64(cmd.Dataoff)); err != nil {
				return err
			}
		}
	}

	return nil
}

func excludeMachoCodeSignature(r io.Reader) io.Reader {
	_, cmd, ok := findMachoCodeSignature(r)
	if !ok {
		return r
	}
	return &excludedReader{r, 0, int64(cmd.Dataoff), int64(cmd.Dataoff + cmd.Datasize)}
}

// excludedReader包装了一个io。读者从它读取将返回基础读取器
// 的字节，但当字节偏移量在开始和结束之间的
// 范围内时，它将返回零字节。
type excludedReader struct {
	r          io.Reader
	off        int64 // 当前偏移量
	start, end int64 // 要排除的范围（读取为零）
}

func (r *excludedReader) Read(p []byte) (int, error) {
	n, err := r.r.Read(p)
	if n > 0 && r.off+int64(n) > r.start && r.off < r.end {
		cstart := r.start - r.off
		if cstart < 0 {
			cstart = 0
		}
		cend := r.end - r.off
		if cend > int64(n) {
			cend = int64(n)
		}
		zeros := make([]byte, cend-cstart)
		copy(p[cstart:cend], zeros)
	}
	r.off += int64(n)
	return n, err
}

func findMachoCodeSignature(r any) (*macho.File, codesign.CodeSigCmd, bool) {
	ra, ok := r.(io.ReaderAt)
	if !ok {
		return nil, codesign.CodeSigCmd{}, false
	}
	f, err := macho.NewFile(ra)
	if err != nil {
		return nil, codesign.CodeSigCmd{}, false
	}
	cmd, ok := codesign.FindCodeSigCmd(f)
	return f, cmd, ok
}
